#!/usr/local/bin/python
#-*- coding: utf-8 -*-
import re, time, logging, gzip
from shared import trans  
try: import cStringIO as strio
except ImportError: import StringIO as strio

from google.appengine.api import urlfetch, memcache, urlfetch_errors
from google.appengine.ext import webapp
from google.appengine.runtime import apiproxy_errors

__author__ = "lyricconch@gmail.com"
__version__ = "1.0.0a"

SERVER_ID= "WallProxyMod/1.0.0a"
NAME_SPACE= "WPM_PROXY"   
RSA_KEY= (0x6904401923F92CB14D9479C9CC4B2E2644964998102B8E6BCAEF2FD6EBBF31A3,
          0x4E2936DAC0239D014F5E7A3A2C7255166FF990DF37BACFA331FAB35261A451A5,0)
ONERROR_HEADERS= """
Content-Type: text/html
Server: %s 
"""% SERVER_ID
ONERROR_CONTENT= """
    <html><head>
        <title>Router Error</title>
        </head><body>
        <h1>Router Error</h1>
        <p>Error Code: %s</p>
        <p>Message: %s</p>
    </body></html>
"""
MODHDRS_REQUEST= {
    'connection':"close", 
    'keep-alive': None, 
    'proxy-authenticate': None,
    'proxy-connection': None,
    'proxy-authorization': None,
    'te': None,
    'trailers': None,
    'transfer-encoding': None, 
    'upgrade': None, 
}
MODHDRS_RESPONSE= {
    'connection':'close',
    'keep-alive': None,
    'te': None,
    'upgrade': None,
    'transfer-encoding': None,
}      
def handle_rpc(rpc, response):
    meth, path, head, body= response[0:4]; rets= None;
    try: rets= rpc.get_result(); logs="Succeed"; 
    except urlfetch_errors.Error, ex:        
        logging.warn("Urlfetch: 1: %s"%repr(ex))
        logs= "Error Urlfetch" "\t.E:%s;"%repr(ex); code=440;
        if meth == "GET":
            try: # perform a Head request for client further use, fail safe.
                logging.warning(str(response))
                rets= urlfetch.fetch(path,"","HEAD",head,True,False,1); logs+="\t.H;"
                logging.warn("urlfetch %s,%s,%s", rets.status_code, rets.headers, rets.content[:100])
                if rets.status_code in (300,301,302,304,307): 
                    logs="Succeed: Redirect:"+logs;
                elif rets.status_code in (200,203,206) or \
                isinstance(ex, urlfetch_errors.ResponseTooLargeError):
                    head['range']= "bytes=0-499999"                
                    rets= urlfetch.fetch(path,"","GET",head,False,"cookie" not in head,10)
                    if rets.status_code in (200,203,206):
                        logs="Continue: "+logs+"\t.R:500;"
                else: rets= urlfetch.fetch(path, "", "GET", head, False, False, 3);logs="Succeed: "+logs              
            except Exception, ex2: logging.warn("Urlfetch: 2: %s,%s"%(repr(ex),repr(ex2)));                     
    except apiproxy_errors.Error, ex: 
        logging.warn("Apiproxy: 1: %s"%repr(ex))
        logs= "Error Apiproxy\t.E:%s;"%repr(ex); code=540; 
    if rets:      
        code= rets.status_code;
        headers= rets.headers
        content= rets.content 
        if rets.final_url:
            logs+= "\t.R:%s;"%rets.final_url  
        if rets.content_was_truncated:
            logs+= "\t.T;"
        if 'set-cookie' in headers:
            headers['set-cookie'], temp= re.subn(", (?=[^,;]+=)", 
                "\r\nSet-Cookie: ", headers['set-cookie']) 
            logs+="\t.C:%d;"%(temp+1)       
        if 'content-encoding' not in headers and \
        "text" in headers.get("content-type",""):
        # and 4*1024<=len(content)<=256*1024:
            temp=strio.StringIO()
            gzip.GzipFile(mode="wb",fileobj=temp, compresslevel=1).write(content)
            content= temp.getvalue()
            headers['content-encoding']= "gzip"
            headers['content-length']= str(len(content))
            logs+="\t.Z;"   
    else: 
        headers= ONERROR_HEADERS        
        content= ONERROR_CONTENT%(code, ex.__doc__)   
    logging.debug("response: len=%d, code=%s, logs=%s, \nhead= %s",
        len(content), code, logs, headers)  
    response[0:4]= str(code), logs, trans.hdr2str(headers,MODHDRS_RESPONSE), content   
    # "max-age" "max-stale" "min-fresh" "private" "max-age"
    # we dont care about no-transform since we never modify content-*, 
    # only-if-cached is useless since memcache is not persist storage,
    # we only focus on private since GOOGLE will deal with the public    
    if rets and logs[0]=="S":
        chctl= "p:%s;c:%s;A:%s;C:%s;"% (head.get("progma"), 
            head.get('cache-control'), headers.get('age'), headers.get('cache-control'))
        if "no-cache" in chctl or "no-store" in chctl: pass;
        elif "revalidate" in chctl: 
            memcache._CLIENT.delete(path, namespace=NAME_SPACE)   
            logging.info("memcache: del: path=%s") 
        elif code in (404,304,204) and meth!="HEAD" and len(body)<16000:
            # headers cache for duplicate request
            memcache._CLIENT.set(path, (meth, head, body, response),
                time=600, namespace=NAME_SPACE)            
            logging.info("memcache: set: path=%s, \nmeth=%s, head=%s, \ncode=%s, headers=%s",
                path, meth, head, code, headers)
        elif code in (200,203,206) and meth=="GET" and (                                                     
        "private" in chctl and 16000<len(content)<800000 or
        "public"  in chctl and  2000<len(content)<100000 ): 
            # entity cache for 304 response
            try: expire=int(re.search(r"C:.*?max-age=\s*(\d*)", chctl).group(1)+
                time.mktime(time.strptime(headers["date"], "%a, %d %b %Y %H:%M:%S GMT"))) 
            except: expire=time.time()+60;
            lastmod, etags= headers.get('last-modified',"N/A"), headers.get('etags',"N/A")
            memcache._CLIENT.set(path, (expire, content, lastmod, etags),
                time=headers.get("expire",3600*72), namespace=NAME_SPACE); 
            logging.info("memcache: set: path=%s, expire=%s, lenentity=%s, lastmod=%s, etags=%s",
                path, time.ctime(expire), len(content), lastmod, etags)
        
def create_attach(response):
    attach= strio.StringIO(response[3])
    prefix= "%010.03f"%(time.time()%1000000)
    sequeue=0; mapping={};    
    while True:   
        part= attach.read(1000000)
        if not part or sequeue%-20==1: 
            if memcache._CLIENT.set_multi(mapping, 3600, prefix+"/"):
                logging.error("memcache fail")
                raise RuntimeError,"memcache fail"            
            if not part: break;
            mapping= {}            
        sequeue+= 1      
        mapping[str(sequeue)]= part           
    logging.debug("memcache: add: prefix/sequeue=%s/%s", prefix, sequeue)     
    response[1]+= "\t.A:%s,%s;"%(prefix,sequeue);
    response[3]= ""       

class MainHandler(webapp.RequestHandler):                
    def post(self):     
        package=[]; rpcs= [];
        session= trans.unpack(self.request.body_file, package, RSA_KEY, None)
        dl= 1+1.384**(5-len(package)) 
        for request in package:
            request[0]= request[0].lstrip("~!@#$%^&*")
            request[2]= trans.str2hdr(request[2],MODHDRS_REQUEST)
            method, path, headers, content= request                                
            logging.debug("request: len=%d, method=%s, path=%s, \nheader= %s", 
                len(content), method, path, headers)            
            cached= memcache._CLIENT.get(path, namespace=NAME_SPACE)
            chctl= headers.get("cache-control","")+headers.get("progma","")
            if cached is not None and "no-cache" not in chctl:
                logging.info("memcache: get: path=%s cached=%s", path, cached)
                if isinstance(cached[0],str):
                    if cached[0]==method and cached[2]==content and cached[1]==str(headers):
                        logging.info("memcache: hit: Head, path=%s resp=%s"%(path,request))
                        request[:]= cached[3]; continue;      
                elif cached[3] in headers.get("if-none-match","") and \
                cached[2] in headers.get("if-modified-since",""):                
                    limit, now= int((re.findall("\d+",chctl)+(0))[0]), time.time()
                    if not chctl or "max-age" in chctl and now+limit<cached[0]:                     
                        logging.info("memcache: hit: Entity, path=%s resp=%s"%(path,request))
                        request[:]= "304","Succeed\t.M;",time.strftime(
                            "Date: %a %d %b %Y %H:%M:%S GMT",time.gmtime()),""; continue;                            
            rpc= urlfetch.create_rpc(dl+3 if content else dl+1 if "?" in path else dl)
            rpc.callback= lambda rpc=rpc,ctx=request: handle_rpc(rpc,ctx)
            urlfetch.make_fetch_call(rpc, path, content, method, headers,
                method == "GET", "cookie" not in headers)
            rpcs.append(rpc)
        for rpc in rpcs: rpc.wait();        
        while True:
            sizes= [len(response[3]) for response in package]
            if sum(sizes)<1048576*8 and max(sizes)<4000000: break;
            create_attach(package[sizes.index(max(sizes))])                          
        self.response.headers['Content-Type']= "application/octet-stream"
        self.response.headers['connection']=" close"        
        trans.packit(self.response.out, package, RSA_KEY, session)
        return        
        
    def get(self):
        if self.request.params:
            prefix= self.request.params.get('prefix',"")
            sequeue= self.request.params.get('sequeue',"")
            content= memcache._CLIENT.get(str("%s/%s"%(prefix,sequeue)))   
            memio= memcache._CLIENT.delete(str("%s/%s"%(prefix,int(sequeue)-1)))
            if content!=None or memio==2: pass; 
            else: return self.response.set_status(404);
            self.response.headers['content-type']= "application/octet-stream"            
            self.response.headers['connection']= "close"
            return self.response.out.write(content or "")                       
        self.response.headers["Content-Type"] = "text/html; charset=utf-8"
        self.response.out.write(r'''
<html><head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>WallProxyMod已经在工作了</title>
</head><body>
    <table width="800" border="0" align="center">
        <tr><td align="center"><hr></td></tr>
        <tr><td align="center">
            <b><h1>%s 已经在工作了</h1></b>
        </td></tr>
        <tr><td align="center"><hr></td></tr>
        <tr><td align="left">
            WallProxyMod是一个开源的HTTP Proxy软件,使用Python编写,运行于Google App Engine平台上.<p> 
            源代码从 GAppProxy 和 WallProxy 参照而来, 并加入一些修正:<p>
            1. 可以支持 RSA+FAST 加密, RSA 保护头信息和会话密钥, FAST以会话密钥保护内容.<p> 
               HTTP/HTTPS内容将难以被观测. (Python代码)<p>
            2. 完善证书欺骗, 避免了2个小bug, 请注意, 经GAE的HTTPS并不能保持原有证书. <p>
               HTTPSoverGAE并不是安全连接, 而只是一个视觉欺骗!(需要 2.6+ 与 PyOpenSSL)<p>
            3. 使用底层的select和socket实现 CONNECT和DirectFetch, 更好的性能.<p>
            3. 智能地并发频繁请求, 减少TCP/HTTP/Encrypt的资源消耗.<p>
            3. 支持私有缓存和部分的公共缓存于200,404等, GOOGLE将缓存多数的公共缓存<p>
            4. 支持代理自动切换(感谢autoproxy-gfwlist.googlecode.com), <p>
               并重新优化判别速度(比adblockplus稍好).<p>
            5. 修改了大文件下载策略, 更好地支持大文件透明传输.<p>
            6. 本版本尚处于测试阶段.                 <p>
        </td></tr>
        <tr><td align="center"><hr></td></tr>
        <tr><td align="center">
            更多GAppProxy介绍,请参考<a href="http://gappproxy.googlecode.com/">GAppProxy项目主页</a>.<p> 
            更多WallProxy介绍,请参考<a href="http://wallproxy.googlecode.com/">WallProxy项目主页</a>. <p> 
        </td></tr>
        <tr><td align="center"><hr></td></tr>
        <tr><td align="center">
            <img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif" alt="Powered by Google App Engine" />
        </td></tr>
        <tr><td align="center"><hr></td></tr>
    </table>
</body></html>
''' %SERVER_ID)      
        
    

def main():
    from google.appengine.ext.webapp.util import run_wsgi_app
    run_wsgi_app(webapp.WSGIApplication([
        (r'/.*', MainHandler)
    ], debug=True))
    
if __name__ == '__main__':
    main()

        